/*
Copyright 2010 Sieric
http://sieric.ca

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package savior.engine;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.Semaphore;
import savior.fs.FSNotSupportedOperationException;
import savior.fs.FSShouldRetryException;
import savior.fs.FileDescription;

import savior.fs.FileSystem;
import savior.messaging.Manager;

public class Backup {

    private Manager log = Manager.getInstance();
    private FileSystem source, destination;
    private List<String> ignoreList;
    private Semaphore blockSem = new Semaphore(0);
    private boolean block = false;

    public Backup(FileSystem _source, FileSystem _destination, List<String> _ignoreList) {
        source = _source;
        destination = _destination;
        ignoreList = _ignoreList;

        // Check if the ignore list is good
        if (ignoreList == null) {
            ignoreList = new LinkedList<String>();
        }
    }

    private Boolean checkDirectory(String _fullPath) {
        List<FileDescription> sourceFiles;
        List<FileDescription> destinationFiles;
        Boolean restart;

        log.information("In the directory: " + _fullPath);

        // Check if in the ignore list
        if (isInList(ignoreList, _fullPath)) {
            log.information("Ignore this current dir: " + _fullPath);
            restart = true;
            while (restart) {
                restart = false;
                try {
                    // Delete the destination directory if it exists
                    if (destination.directoryExists(_fullPath)) {
                        deleteDirectoryRecursivly(_fullPath);
                    }
                    return true;
                } catch (FSShouldRetryException ex) {
                    restart = true;
                }
            }
        }

        restart = true;
        while (restart) {
            restart = false;
            try {
                // Check if it still exists
                if (!source.directoryExists(_fullPath)) {
                    return false;
                }
                if (!destination.directoryExists(_fullPath)) {
                    destination.createDirectory(_fullPath);
                }
                sourceFiles = source.listFiles(_fullPath);
                checkIfPause();
                // Check if the listing couldn't be done
                if (sourceFiles == null) {
                    return false;
                }
                destinationFiles = destination.listFiles(_fullPath);
                checkIfPause();
                // Remove all the source files to ignore
                ListIterator<FileDescription> it = sourceFiles.listIterator();
                while (it.hasNext()) {
                    checkIfPause();
                    FileDescription next = it.next();
                    if (isInList(ignoreList, _fullPath + next.name)) {
                        log.information("Ignore this file " + _fullPath + next.name);
                        it.remove();
                    }
                }
                // Create the file list
                List<String> sourceFilesNames = new ArrayList<String>();
                for (FileDescription fd : sourceFiles) {
                    sourceFilesNames.add(fd.name);
                }
                // Delete the destination's files that are not in the source
                it = destinationFiles.listIterator();
                while (it.hasNext()) {
                    checkIfPause();
                    FileDescription next = it.next();
                    if (!isInList(sourceFilesNames, next)) {
                        log.action("Deleting " + _fullPath + next.name);
                        destination.deleteFile(_fullPath + next.name);
                        it.remove();
                    }
                }
                // Create the file list
                List<String> destinationFilesNames = new ArrayList<String>();
                for (FileDescription fd : destinationFiles) {
                    destinationFilesNames.add(fd.name);
                }
                // Copy the source's files that are not in the destination
                it = sourceFiles.listIterator();
                while (it.hasNext()) {
                    checkIfPause();
                    FileDescription next = it.next();
                    if (!isInList(destinationFilesNames, next)) {
                        copyFile(_fullPath, next, 0);
                        it.remove();
                    }
                }
                // Compare the files date and copy those that the source are newer
                it = sourceFiles.listIterator();
                while (it.hasNext()) {
                    checkIfPause();
                    FileDescription sourcefd = it.next();
                    FileDescription destfd = getFileDescription(destinationFiles, sourcefd.name);
                    long srcTime = sourcefd.time;
                    long dstTime = destfd.time;
                    // If the destination is more recent
                    if (srcTime < dstTime) {
                        // If the destination is smaller
                        if (sourcefd.size > destfd.size) {
                            copyFile(_fullPath, sourcefd, destfd.size);
                            it.remove();
                            continue;
                        }
                    }
                    if (srcTime > dstTime) {
                        copyFile(_fullPath, sourcefd, 0);
                        it.remove();
                    }
                }
                // Compare the files size and copy those that are not the same size
                it = sourceFiles.listIterator();
                while (it.hasNext()) {
                    checkIfPause();
                    FileDescription next = it.next();
                    if (next.size != getFileDescription(destinationFiles, next.name).size) {
                        copyFile(_fullPath, next, 0);
                        it.remove();
                    }
                }
                // List all the source's directories
                List<String> sourceDirectories = source.listDirectories(_fullPath);
                checkIfPause();
                // Check if the listing couldn't be done
                if (sourceFiles == null) {
                    return false;
                }
                List<String> destinationDirectories = destination.listDirectories(_fullPath);
                checkIfPause();
                // Delete the destination's directories that are not in the source
                ListIterator<String> itd = destinationDirectories.listIterator();
                while (itd.hasNext()) {
                    checkIfPause();
                    String next = itd.next();
                    if (!isInList(sourceDirectories, next)) {
                        deleteDirectoryRecursivly(_fullPath + next);
                        itd.remove();
                    }
                }
                // Check the source's directories
                itd = sourceDirectories.listIterator();
                while (itd.hasNext()) {
                    checkIfPause();
                    String next = itd.next();
                    if (!checkDirectory(_fullPath + next)) {
                        restart = true;
                    }
                }
            } catch (FSShouldRetryException ex) {
                restart = true;
            }
        }

        return true;
    }

    private void checkIfPause() {
        if (block) {
            log.information("Backup paused");
            try {
                blockSem.acquire();
            } catch (InterruptedException e) {
                log.error("DEBUG: Semaphore problem");
            }
            log.information("Backup resumed");
        }
    }

    private Boolean copyFile(String basePath, FileDescription _source, long _destSize) {
        boolean result = true;
        boolean retry = true;
        boolean localretry;
        String fullpath = basePath + _source.name;

        if (_destSize > 0) {
            log.action("Resuming " + fullpath);
        } else {
            log.action("Copying " + fullpath);
        }

        while (retry) {
            checkIfPause();
            result = true;

            // Get the input
            InputStream in = null;
            localretry = true;
            while (localretry) {
                localretry = false;
                try {
                    in = source.getFile(fullpath, _destSize);
                } catch (FSShouldRetryException ex) {
                    localretry = true;
                }
            }
            if (in == null) {
                result = false;
            }

            // Get the output
            OutputStream out = null;
            if (result) {
                localretry = true;
                while (localretry) {
                    localretry = false;
                    try {
                        out = destination.putFile(fullpath, _destSize != 0); // Append if not at the start
                    } catch (FSShouldRetryException ex) {
                        localretry = true;
                    }
                }
            }
            if (out == null) {
                result = false;
            }

            // Make the bridge till the end
            // If disconnection, ask again
            int len = 0;
            byte buf[] = new byte[1024];
            try {
                while (((len = in.read(buf)) >= 0) && result) {
                    // Send the bytes
                    out.write(buf, 0, len);

                    // Increment the counter
                    _destSize += len;
                }
            } catch (Exception e) {
                retry = true;
            }


            if (len == -1) {
                retry = false;
            }

            // Close the connections
            try {
                in.close();
            } catch (Exception e1) {
            }
            try {
                out.close();
            } catch (Exception e1) {
            }
        }

        // Change the time
        if (result) {
            localretry = true;
            while (localretry) {
                localretry = false;
                try {
                    if (!destination.setFileTime(fullpath, _source.time)) {
                        log.error("Could not set the time for " + fullpath);
                    }
                } catch (FSShouldRetryException ex) {
                    localretry = true;
                } catch (FSNotSupportedOperationException ex) {
                }
            }
        }

        if (!result) {
            log.error("Could not copy " + fullpath);
        }

        log.action("Done Copying " + fullpath);

        return result;
    }

    private Boolean deleteDirectoryRecursivly(String _fullPath) {
        boolean retry;
        // Delete all the files in the directory
        List<FileDescription> destinationFiles = null;
        retry = true;
        while (retry) {
            retry = false;
            try {
                destinationFiles = destination.listFiles(_fullPath);
            } catch (FSShouldRetryException ex) {
                retry = true;
            }
        }
        if (destinationFiles == null) {
            return true;
        }
        ListIterator<FileDescription> itf = destinationFiles.listIterator();

        while (itf.hasNext()) {
            checkIfPause();
            FileDescription nextfd = itf.next();
            retry = true;
            while (retry) {
                retry = false;
                try {
                    destination.deleteFile(_fullPath + nextfd.name);
                } catch (FSShouldRetryException ex) {
                    retry = true;
                }
                log.action("Deleting " + _fullPath + nextfd.name);
            }
        }

        // Delete recursivly all the directories
        List<String> destinationDir = null;
        retry = true;
        while (retry) {
            retry = false;
            try {
                destinationDir = destination.listDirectories(_fullPath);
            } catch (FSShouldRetryException ex) {
                retry = true;
            }
        }
        if (destinationDir == null) {
            return true;
        }
        ListIterator<String> itd = destinationDir.listIterator();

        while (itd.hasNext()) {
            checkIfPause();
            String nextStr = itd.next();
            deleteDirectoryRecursivly(_fullPath + nextStr);
            log.action("Deleting " + _fullPath + nextStr);
        }


        // Delete this directory
        boolean result = false;
        retry = true;
        while (retry) {
            retry = false;
            try {
                result = destination.deleteDirectory(_fullPath);
            } catch (FSShouldRetryException ex) {
                retry = true;
            }
        }
        log.action("Deleting " + _fullPath);
        if (!result) {
            log.error("Could not delete " + _fullPath);
        }

        return result;
    }

    private FileDescription getFileDescription(List<FileDescription> destinationFiles, String name) {
        for (FileDescription fd : destinationFiles) {
            if (fd.name.equals(name)) {
                return fd;
            }
        }

        return null;
    }

    private Boolean isInList(List<String> _list, FileDescription _token) {
        return isInList(_list, _token.name);
    }

    private Boolean isInList(List<String> _list, String _token) {
        ListIterator<String> it = _list.listIterator();

        while (it.hasNext()) {
            String next = it.next();
            if (next.compareTo(_token) == 0) {
                return true;
            }
        }

        return false;
    }

    public void start() {
        // Check if everything is alright
        if (!source.testConnection()) {
            log.error("The source is invalid");
            return;
        }
        if (!destination.testConnection()) {
            log.error("The destination is invalid");
            return;
        }

        // Start
        long start = System.currentTimeMillis();
        log.information("Start of backup");
        try {
            checkDirectory("");
        } catch (Exception e) {
            e.printStackTrace();
            log.error("Sorry, a strange error happenned. Backup stopped");
        }
        log.information("End of backup in " + (System.currentTimeMillis() - start) / 1000 + " seconds");
    }

    public boolean togglePause() {
        // Change the bool
        block = !block;

        // Release the semaphore
        if (!block) {
            log.information("The backup will be resumed if it was paused");
            if (blockSem.hasQueuedThreads()) {
                blockSem.release(1);
            }
        } else {
            log.information("The backup will be paused after the current transfert");
        }

        return block;
    }
}
